#########################
#   Author : Lion yu    #
#   Date : 2023/05/05   #
#   Id : SM2772         #
#########################
import os
from string import Template
import threading
import datetime
import time
import json
import requests
import jwt
import toml
import nacos
from nacos import NacosClient
import pymysql
import logging
from logging.handlers import TimedRotatingFileHandler
import colorlog
from conf.conf import log_path
import pandas as pd

# 数据静态文件路径
basedata_path = os.path.abspath(os.path.dirname(__file__))
basedata_path = os.path.join(basedata_path, 'BaseDatas')

log_colors_config = {
    'DEBUG': 'cyan',
    'INFO': 'green',
    'WARNING': 'yellow',
    'ERROR': 'red',
    'CRITICAL': 'red',
}
now_t = time.strftime("%Y-%m-%d", time.localtime(int(time.time())))
log_file = os.path.join(log_path, f'LogInfo_{now_t}.log')
errorLog_file = os.path.join(log_path, f'LogError_{now_t}.log')
logging.basicConfig(
    format='[%(asctime)s] %(filename)s -> %(funcName)s line:%(lineno)d [%(levelname)s] : %(message)s',
    level=logging.INFO
    )

# class MyNacos():
#     def __init__(self) -> None:
#         self.NACOS_HOST = "mse-a8c55a50-nacos-ans.mse.aliyuncs.com"
#         self.NACOS_PORT = 8848
#         self.NAMESPACE = "9ed34136-9b76-421a-8351-27d47986a6ef"
#         self.NACOS_ACCESS_KEY = "LTAI4FjKhHWU9aZPg6UPzPYv"
#         self.NACOS_SECRET_KEY = "y7vrVh1hfMK2BuhYBD1xVrsdS1o8tr"
#         self.data_id = "tbstatisticlview"
#         self.group = "tbstatisticlview"
    
#     def getConfig(self):
#         client = nacos.NacosClient(
#                                     self.NACOS_HOST, 
#                                     namespace=self.NAMESPACE, 
#                                     sk=self.NACOS_SECRET_KEY, 
#                                     ak=self.NACOS_ACCESS_KEY
#                                     )
#         confResp = client.get_config(data_id=self.data_id, group=self.group, no_snapshot=None)
#         return toml.loads(confResp)

class MyNacos():
    def __init__(self) -> None:
        self.NACOS_HOST = "mse-a8c55a50-nacos-ans.mse.aliyuncs.com"
        self.NACOS_PORT = 8848
        self.NAMESPACE = "9ed34136-9b76-421a-8351-27d47986a6ef"
        self.NACOS_ACCESS_KEY = "LTAI4FjKhHWU9aZPg6UPzPYv"
        self.NACOS_SECRET_KEY = "y7vrVh1hfMK2BuhYBD1xVrsdS1o8tr"
        self.data_id = "tbstatisticlview"
        self.group = "tbstatisticlview"
        
        # 配置存储和锁
        self.config = {}
        self.config_lock = threading.RLock()
        
        # 监听线程和标志
        self.listening = False
        self.listener_thread = None
        
        # 初始化客户端和配置
        self.client = None
        self.init_client()
        self.load_config()
        
        # 启动监听
        self.start_listening()
    
    def init_client(self):
        """初始化Nacos客户端"""
        try:
            # 注意：根据nacos-sdk-python的实际API调整参数
            # 可能需要使用server_addresses参数而非host/port
            self.client = NacosClient(
                server_addresses=f"{self.NACOS_HOST}:{self.NACOS_PORT}",
                namespace=self.NAMESPACE,
                ak=self.NACOS_ACCESS_KEY,
                sk=self.NACOS_SECRET_KEY
            )
            logging.info("Nacos客户端初始化成功")
        except Exception as e:
            logging.error(f"Nacos客户端初始化失败: {str(e)}")
            raise
    
    def load_config(self):
        """从Nacos加载配置"""
        try:
            conf_resp = self.client.get_config(
                data_id=self.data_id, 
                group=self.group
            )
            with self.config_lock:
                self.config = toml.loads(conf_resp)
            logging.info("配置加载成功")
            return self.config
        except Exception as e:
            logging.error(f"配置加载失败: {str(e)}")
            raise
    
    def get_config(self):
        """获取配置（线程安全）"""
        with self.config_lock:
            return self.config.copy()  # 返回副本避免外部修改影响
    
    def config_listener(self, data_id, group, config):
        """配置变更监听器"""
        try:
            logging.info(f"配置已变更，正在更新... dataId: {data_id}, group: {group}")
            new_config = toml.loads(config)
            with self.config_lock:
                self.config = new_config
            logging.info("配置更新成功")
        except Exception as e:
            logging.error(f"配置更新失败: {str(e)}")
    
    def start_listening(self):
        """启动配置监听"""
        if self.listening:
            return
            
        self.listening = True
        try:
            # 添加配置监听器（根据实际API调整）
            self.client.add_config_watcher(
                data_id=self.data_id,
                group=self.group,
                cb=self.config_listener
            )
            logging.info("Nacos配置监听已启动")
        except Exception as e:
            logging.error(f"配置监听启动失败: {str(e)}")
            self.listening = False
    
    def stop_listening(self):
        """停止配置监听"""
        self.listening = False
        try:
            # 移除配置监听器（根据实际API调整）
            self.client.remove_config_watcher(
                data_id=self.data_id,
                group=self.group,
                cb=self.config_listener
            )
            logging.info("Nacos配置监听已停止")
        except Exception as e:
            logging.error(f"配置监听停止失败: {str(e)}")
    
    def refresh_config(self):
        """手动刷新配置"""
        logging.info("手动触发配置刷新")
        return self.load_config()

# 配置读取
# conf_path = os.path.abspath(os.path.dirname(__file__))
# conf_path = os.path.join(conf_path, 'conf/con.toml')
# config = toml.load(conf_path)
# config = MyNacos().getConfig()

# config = MyNacos()
# config = config.getConfig()

nacos_client = MyNacos()
config = nacos_client.get_config()

# ZooKeeper配置
ZkConf = config['ZooKeeper']
zkHost = ZkConf['ZK_colonyHost']
zkPort = ZkConf['ZK_Port']

# 钉钉机器人配置
DingTalk_Secret = config['DingTalk']['secret']
DingTalk_AccessToken = config['DingTalk']['access_token']
picPath = config['DingTalk']['picPath'] # 钉钉通知头图

# 定时任务配置
coreTime = config['CORE']['time']
# TbConf配置
TbConf = config['TbConf']
# TbApis配置
TbApis = config['TbApis']
# 数据库配置
DBConf = config['DBConf']

# 项目配置
ProjectConfig:dict = config['ProjectConfig']

BugBI = config['BugBI']

class CommonLog():
    def __init__(self):
        self.logger = logging.getLogger()
        self.logger.setLevel(logging.INFO)
        self.formatter = colorlog.ColoredFormatter('%(log_color)s[%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s',log_colors=log_colors_config)
        new_formatter = '[%(levelname)s]%(asctime)s:%(msecs)s.%(process)d,%(thread)d#>[%(funcName)s]:%(lineno)s  %(message)s'
        """
        %(asctime)s 字符串形式的当前时间。默认格式是“2021-09-08 16:49:45,896”。逗号后面的是毫秒
        %(created)f 时间戳, 等同于time.time()
        %(relativeCreated)d 日志发生的时间相对于logging模块加载时间的相对毫秒数
        %(msecs)d 日志时间发生的毫秒部分
        %(levelname)s 日志级别str格式
        %(levelno)s 日志级别数字形式(10, 20, 30, 40, 50)
        %(name)s 日志器名称, 默认root
        %(message)s 日志内容
        %(pathname)s 日志全路径
        %(filename)s 文件名含后缀
        %(module)s 文件名不含后缀
        %(lineno)d 调用日志记录函数源代码的行号
        %(funcName)s 调用日志记录函数的函数名
        %(process)d 进程id
        %(processName)s 进程名称
        %(thread)d 线程ID
        %(threadName)s 线程名称
        """
        fmt = logging.Formatter(new_formatter)

        log_handel = TimedRotatingFileHandler(log_file, when='MIDNIGHT')
        log_handel.setFormatter(fmt)
        self.logger = logging.getLogger('info')
        self.logger.setLevel(logging.INFO)
        self.logger.addHandler(log_handel)
        
        ##################### 错误日志 #########################
        self.Errorlogger = logging.getLogger()
        self.Errorlogger.setLevel(logging.ERROR)
        ErrorLog_handel = TimedRotatingFileHandler(errorLog_file, when='MIDNIGHT')
        ErrorLog_handel.setFormatter(fmt)
        self.Errorlogger = logging.getLogger('error')
        self.Errorlogger.setLevel(logging.ERROR)
        self.Errorlogger.addHandler(ErrorLog_handel)
        
 
    def console(self, level, message):
        ch = logging.StreamHandler()
        ch.setLevel(logging.DEBUG)
        ch.setFormatter(self.formatter)
        self.logger.addHandler(ch)
    
        if level == 'info':
            self.logger.info(message)
        if level == 'debug':
            self.logger.debug(message)
        if level == 'warning':
            self.logger.warning(message)
        if level == 'error':
            # self.logger.error(message)
            self.Errorlogger.error(message)
            self.Errorlogger.removeHandler(ch)
        self.logger.removeHandler(ch)
        return

    def debug(self, msg):
        self.console('debug', msg)

    def info(self, msg):
        self.console('info', msg)

    def warning(self, msg):
        self.console('warning', msg)

    def error(self, msg):
        self.console('error', msg)
        
Log = CommonLog()


class Handle(object):
    uri = "https://open.teambition.com"
    appId = TbConf['appId']
    Tenant_type = TbConf['Tenant_type']
    Tenant_Id = TbConf['Tenant_Id']
    appId = TbConf['appId']
    appSecret = TbConf['appSecret']
    
    # 特殊基础数据 文件路径(暂不用)
    specialBaseData_path = os.path.join(basedata_path, 'SpecailDatas')
    # task基础数据 文件路径
    taskData_path = os.path.join(basedata_path, 'TaskDatas')
    # bug基础数据 文件路径
    bugData_path = os.path.join(basedata_path, 'BugDatas')
    # bug 待处理基础数据 文件路径
    bugDataExt_path = os.path.join(basedata_path, 'BugDatas/Bug_BaseDatas')
    # bug快照数据 文件路径
    bugSnapshotData_path = os.path.join(basedata_path, 'BugSnapshotDatas')
    # 周统计数据 文件路径
    statistics2Week_path = os.path.join(basedata_path, 'StatisticsDatas/Week')
    # 月统计数据 文件路径
    statistics2Month_path = os.path.join(basedata_path, 'StatisticsDatas/Month')
    
    # 用户免登授权
    @staticmethod
    def getUserAccessToken(appId=appId, appSecret=appSecret):
        iat_timestamp = int(time.time())
        exp_timestamp = iat_timestamp + 3600*1000
        payload = {
            "_appId" : appId, 
            "iat" : iat_timestamp,
            "exp" : exp_timestamp
            }
        return "Bearer " + jwt.encode(payload, appSecret, algorithm="HS256")
    
    # TBOpenApi request封装
    @staticmethod
    def api_req(req_info:dict):
        if req_info is None:
            Log.error(f"def api_req -> req_info : {req_info}")
            return None
        method = req_info['method']
        if method not in ["PUT", "POST", "GET"]:
            return None
        headers = {
            "Content-Type": "application/json",
            "Authorization" : Handle.getUserAccessToken(),
            "X-Tenant-Id": Handle.Tenant_Id,
            "X-Tenant-Type": "organization"
        }
        try:
            path = req_info['path']
            body = {}
            if 'body' in req_info:
                body = req_info['body']
            url = Handle.uri + path
            if method == "GET":
                req = requests.get(url=url, headers=headers, params=body)
                try:
                    req_ = json.loads(req.text)
                except Exception as e:
                    Log.error(e)
                    Log.error(req.text)
                return req_
            if method == "POST":
                req = requests.post(url=url, headers=headers, json=body)
                return req.json()
            if method == "PUT":
                
                return 
        except KeyError as e:
            Log.error(f"Error ===> {e} is empty")
            return None
    
    # str 转 datatime
    @staticmethod
    def str2datetime(data:str):
        return datetime.date(*list(map(int, data.split('-')))).strftime("%Y-%m-%d %H:%M:%S")
    
    # 服务于路径变量替换方法
    @staticmethod
    def template(data:str, change:dict={}):
        if len(change) > 0:
            template = Template(data)
            template_data = template.safe_substitute(change)
            return template_data
        else:
            return data
    
    # 请求内容 模板替换
    @staticmethod
    def req_info(api_index, path_change:dict={}, body_change:dict={}):

        req_info = {}
        try:
            if len(path_change) > 0:
                req_info['path'] = Handle.template(TbApis[api_index]['path'], change=path_change)
            else:
                req_info['path'] = TbApis[api_index]['path']      
            req_info['method'] = TbApis[api_index]['method'] 
            if len(body_change) > 0:
                req_info['body'] = json.loads(Handle.template(TbApis[api_index]['body'], change=body_change))
            else:
                req_info['body'] = json.loads(TbApis[api_index]['body'])
            return req_info
        except Exception as e:
            Log.error(e)
            return
    
    @staticmethod
    def sub_Object(t:list):
        if len(t) == 0:
            return []
        result_t = []
        range_num = 3
        every_num = len(t)//range_num
        num_tmp = 0
        for i in range(range_num-1):
            t_tmp = t[num_tmp:num_tmp+every_num]
            num_tmp += every_num
            result_t.append(t_tmp)
        t_last = t[num_tmp:]
        result_t.append(t_last)
        return result_t

        
    # 数据库连接
    @staticmethod
    def db_connect(sql_text:str, _database=DBConf['database'], _host=DBConf['host'], _user=DBConf['user'], _password=DBConf['password']):
        conn = pymysql.connect(
            host=_host,
            user=_user,
            password=_password,
            database=_database,
            charset="utf8",
            port=3306
            )
        cursor = conn.cursor()
        try:
            cursor.execute(sql_text)
            ll = cursor.fetchall()
        except Exception as e:
            Log.error(e)
        else:
            conn.commit()
        finally:
            cursor.close()
            conn.close()
        return ll
    
    # 表插入
    @staticmethod
    def db_insert(table:str, column:str, value:str):
        sql = f"INSERT INTO {table} {column} VALUES {value};"
        try:
            Handle.db_connect(sql)
            return
        except Exception as e:
            Log.error(f"{table}表 插入数据失败 \n  sql ==> {sql}")
            return
        
    # 表更新
    @staticmethod
    def db_update(table:str, column:str, value:str, where:str=""):
        sql = str()
        if len(where) == 0:
            sql = f"UPDATE {table} SET {column} = {value};"
        else:
            sql = f"UPDATE {table} SET {column} = {value} WHERE {where};"
        try:
            Handle.db_connect(sql)
            return
        except Exception as e:
            Log.error(f"{table}表 更新数据失败 \n  sql ==> {sql} \n {e}")
            return
 
    # 表查询
    @staticmethod
    def db_select(table:str, index:str, where:str=""):
        sql = str()
        if len(where) == 0:
            sql = f"SELECT {index} FROM {table};"
        else:
            sql = f"SELECT {index} FROM {table} WHERE {where};"
        try:
            respSet = Handle.db_connect(sql)
            return respSet
        except Exception as e:
            Log.error(f"{table}表 查询数据失败 \n  sql ==> {sql} \n {e}")
            return
        
    # 表数据删除
    @staticmethod
    def db_delete(table:str, where:str=""):
        sql = str()
        if len(where) == 0:
            sql = f"DELETE FROM {table};"
        else:
            sql = f"DELETE FROM {table} WHERE {where};"
        try:
            respSet = Handle.db_connect(sql)
            return respSet
        except Exception as e:
            Log.error(f"{table}表 删除数据失败 \n  sql ==> {sql} \n {e}")
            return

    # 线程池
    @staticmethod
    def threadPool_main(fun, target:list = []):
        Log.info('线程池主方法执行开始')
        pool = []
        if len(target) == 0:
            thread_one = threading.Thread(target=fun)
            pool.append(thread_one)
        else:
            for i in target:
                thread_one = threading.Thread(target=fun, args=i)
                pool.append(thread_one)
        for n in pool:
            n.start()
        Log.info('线程池主方法执行完毕')
        return
    
    @staticmethod
    def timehock(projectId):
        def timehock_1(fun):
            Log.info(f"projectId : {projectId}  任务数据开始写入")
            s_time = time.time()
            fun()
            e_time = time.time()
            Log.info(f"projectId : {projectId}  任务数据写入完成, 耗时 {round(e_time-s_time, 2)} 秒")
        return timehock_1
    
    @staticmethod
    def readcsv2DF(filepath, subset:list=[], is_fillna:bool=False, is_distinct:bool=False):
        data = pd.read_csv(filepath, index_col=False)
        if "Unnamed: 0" in data.columns.to_list():
            data.drop("Unnamed: 0", axis=1, inplace=True)
        if is_fillna == True:
            data.fillna("", inplace=True)
        if is_distinct == True:
            if len(subset) == 0:
                Log.error("缺少去重字段")
                return
            data.drop_duplicates(subset=subset, keep='last', inplace=True)
        return data